#include <stdio.h>
#include <string.h>

#include "common.h"
#include "scanner.h"

typedef struct {
    const char* start;
    const char* current;
    int line;
} Scanner;

Scanner scanner;

static bool isAtEnd() {
    return *scanner.current == '\0';
}

static Token makeToken(TokenType type) {
    Token token = {
        .type = type,
        .start = scanner.start,
        .length = (int)(scanner.current - scanner.start),
        .line = scanner.line,
    };
    

    return token;
}

static Token errorToken(const char *message) {
    Token token = {
        .type = TOKEN_ERROR,
        .start = message,
        .line = scanner.line,
        .length = strlen(message),
    };
    

    return token;
}

void initScanner(const char *source) {
    scanner.start = scanner.current = source;
    scanner.line = 1;
}

static char advance() {
    scanner.current++;
    return scanner.current[-1];
}

static bool match(char expected) {
    if (isAtEnd()) return false;
    if (*scanner.current != expected) return false;
    scanner.current++;
    return true;
}

static char peek() {
    return *scanner.current;
}

static char peekNext() {
    if (isAtEnd()) return '\0';

    return scanner.current[1];
}

static bool isDigit(char ch) {
    return ch >= '0' && ch <= '9';
}

static bool isAlpha(char ch) {
    return (ch >= 'a' && ch <= 'z') ||
           (ch >= 'A' && ch <= 'Z') ||
            ch == '_';
}

static void skipWhitespace() {
    for(;;) {
        char ch = peek();
        switch (ch)
        {
            case ' ':
            case '\t':
            case '\r':
                advance();
                break;
            case '\n':
                advance();
                scanner.line++;
                break;
            case '/':
                if (peekNext() == '/') {
                    while (peek() != '\n' && !isAtEnd())
                        advance();                    
                } else {
                    return;
                }
                break;
            default:
                return;
        }
    }
}

static Token string() {
    advance();
    while (peek() != '"' && !isAtEnd()) {
        if (peek() == '\n')
            scanner.line++;

        advance();
    }

    if (isAtEnd())
        return errorToken("Unterminated string");

    advance();

    return makeToken(TOKEN_STRING);
}

static Token number() {
    bool hasDot = false;
    char ch = peek();
    while (isDigit(ch) || ch == '.') {        
        if (ch == '.') {
            if (!hasDot) {
                hasDot = true;
            } else {
                break;
            }
        }
        advance();
        ch = peek();
    }
    
    return makeToken(TOKEN_NUMBER);
}

static TokenType checkKeyword(int start, int length, const char *rest, TokenType type) {
    if (scanner.current - scanner.start == start + length 
         && memcmp(scanner.start + start, rest, length) == 0)
        return type;

    return TOKEN_IDENTIFIER;
}

static TokenType identifierType() {
    switch (scanner.start[0]) {
        case 'a': return checkKeyword(1, 2, "nd", TOKEN_AND);
        case 'c': return checkKeyword(1, 4, "lass", TOKEN_CLASS);
        case 'e': return checkKeyword(1, 3, "lse", TOKEN_ELSE);
        case 'f': {
            switch (scanner.start[1]) {
                case 'a': return checkKeyword(2, 3, "lse", TOKEN_FALSE);
                case 'o': return checkKeyword(2, 1, "r", TOKEN_FOR);
                case 'u': return checkKeyword(2, 1, "n", TOKEN_FUN);
            }
        }
        case 'i': return checkKeyword(1, 1, "f", TOKEN_IF);
        case 'n': return checkKeyword(1, 2, "il", TOKEN_NIL);
        case 'o': return checkKeyword(1, 1, "r", TOKEN_OR);
        case 'p': return checkKeyword(1, 4, "rint", TOKEN_PRINT);
        case 'r': return checkKeyword(1, 5, "eturn", TOKEN_RETURN);
        case 's': return checkKeyword(1, 4, "uper", TOKEN_SUPER);
        case 't': {
            switch (scanner.start[1]) {
                case 'h': return checkKeyword(2, 2, "is", TOKEN_THIS);
                case 'r': return checkKeyword(2, 2, "ue", TOKEN_TRUE);
            }
        }
        case 'v': return checkKeyword(1, 2, "ar", TOKEN_VAR);
        case 'w': return checkKeyword(1, 4, "hile", TOKEN_WHILE);
        default: return TOKEN_IDENTIFIER;
    }
}

static Token identifier() {
    while (isAlpha(peek()) || isDigit(peek()))
        advance();

    return makeToken(identifierType());
}

Token scanToken() {
    skipWhitespace();

    scanner.start = scanner.current;
    if (isAtEnd()) 
        return makeToken(TOKEN_EOF);

    char ch = advance();

    if (isDigit(ch))
        return number();

    if (isAlpha(ch))
        return identifier();

    switch (ch) {
        case '(':
            return makeToken(TOKEN_LEFT_PAREN);
        case ')':
            return makeToken(TOKEN_RIGHT_PAREN);
        case '{':
            return makeToken(TOKEN_LEFT_BRACE);
        case '}':
            return makeToken(TOKEN_RIGHT_BRACE);
        case ',':
            return makeToken(TOKEN_COMMA);
        case '.':
            return makeToken(TOKEN_DOT);
        case '-':
            return makeToken(TOKEN_MINUS);
        case '+':
            return makeToken(TOKEN_PLUS);
        case ';':
            return makeToken(TOKEN_SEMICOLON);
        case '/':
            return makeToken(TOKEN_SLASH);
        case '*':
            return makeToken(TOKEN_STAR);
        case '!':
            return match('=') ? makeToken(TOKEN_BANG_EQUAL) : makeToken(TOKEN_BANG);
        case '=':
            return match('=') ? makeToken(TOKEN_EQUAL_EQUAL) : makeToken(TOKEN_EQUAL);
        case '>':
            return match('=') ? makeToken(TOKEN_GREATER_EQUAL) : makeToken(TOKEN_GREATER);
        case '<':
            return match('=') ? makeToken(TOKEN_LESS_EQUAL) : makeToken(TOKEN_LESS);            
        case '"':
            return string();
    }

    return errorToken("Unexpect character.");
}